<?php

/**
 * This is the model class for table "msgg_item".
 *
 * The followings are the available columns in table 'msgg_item':
 * @property integer $id
 * @property string $pid
 * @property string $aid
 * @property string $app_id
 * @property string $bid
 * @property integer $time
 * @property integer $time_renewal
 * @property integer $level
 * @property string $alive
 * @property string $retry
 * @property string $bundle
 * @property string $status
 * @property string $type
 * @property string $channel
 * @property string $params
 * @property string $result
 * @property string $call_back_params
 * @property string $time_create
 * @property string $time_update
 */
class MsggItem extends CActiveRecord implements Transmitter
{
    use util_array;
    use util_person;
    public $searchKey = '';
    public $alarm_type = '';
    public $time_begin = null;
    public $time_end = null;


    const EVENT_TYPE_ON_AFTER_EXECUTE = 'onAfterExecute';
    const EVENT_TYPE_ON_AFTER_HOOK = 'onAfterHook';
    const EVENT_TYPE_ON_BEFORE_EXECUTE = 'onBeforeExecute';

    const INNER_LEVEL = 99;
    const DEFAULT_KEY = 'default';

    const TYPE_MAIL = 'mail';
    const TYPE_CALL = 'call';
    const TYPE_SMS = 'sms';
    const TYPE_WECHAT = 'wechat';
    //
    const STATUS_SUCCESS = 0;
    const STATUS_BUNDLED = 1;
    const STATUS_CANCEL = 2;
    const STATUS_COMPLETE = 3;
    //space 11-19 to define more failure type in the further
    const STATUS_FAILURE = 10;
    const STATUS_TIMEOUT = 11;
    const STATUS_PERSON_OVERCLOCK = 12;
    //space 21-29 to define more error type in the further
    const STATUS_ERROR = 20;

    const STATUS_RUNNING = 30;
    const STATUS_HOOKING = 31;

    const STATUS_WAITING = 40;
    const STATUS_RETRYING = 41;

    const STATUS_INITIAL = 50;

    const BUNDLE_DENY = 0;
    const BUNDLE_ALLOW = 1;
    const BUNDLE_ITEM = 10;

    const ERROR_CODE_ITEM_ATTR_INVALID = 10;
    const ERROR_MSG_ITEM_ATTR_INVALID = 'item attrs invalid';

    const ERROR_CODE_ITEM_TYPE_INVALID = 11;
    const ERROR_MSG_ITEM_TYPE_INVALID = 'item type invalid';

    const ERROR_CODE_ITEM_PARAMS_INVALID = 12;
    const ERROR_MSG_ITEM_PARAMS_INVALID = 'item params invalid';

    const ERROR_CODE_ITEM_CONFIG_INVALID = 13;
    const ERROR_MSG_ITEM_CONFIG_INVALID = 'item config invalid';

    const ERROR_CODE_ITEM_STATUS_INIT_INVALID = 14;
    const ERROR_MSG_ITEM_STATUS_INIT_INVALID = 'msg is no ready to running';

    const ERROR_CODE_ITEM_TIME_OUT_ERROR = 15;
    const ERROR_MSG_ITEM_TIME_OUT_ERROR = 'msg is time out';

    const ERROR_CODE_PERSON_OVER_CLOCK_ERROR = 16;
    const ERROR_MSG_PERSON_OVER_CLOCK_ERROR = 'person is over clock';

    const ERROR_CODE_SEND_ERROR = 17;
    const ERROR_MSG_SEND_ERROR = 'msgs send error';

    const ERROR_CODE_GATEWAY_FAILURE = 20;
    const ERROR_MSG_GATEWAY_FAILURE = 'item send fail';

    const ERROR_CODE_SEND_FAILURE = 21;
    const ERROR_MSG_SEND_FAILURE = 'msgs send fail';

    private $_params = null;
    private $_result = null;
    private $_logs = false;
    private static $_options = array();


    /**
     * @return string the associated database table name
     */
    public function tableName()
    {
        return 'msgg_item';
    }

    /**
     * @return array validation rules for model attributes.
     */
    public function rules()
    {
        return array(
            array('time, status, type', 'required'),
            array('type, channel', 'length', 'max' => 32),
            array('call_back_params', 'length', 'max' => 128),
            array('pid, aid,app_id, bid, alive, retry, bundle, params, result,call_back_params,time_create, time_update', 'safe'),
            array('searchKey,alarm_type', 'safe'),
            array(
                'level',
                'default',
                'value' => 0,
                'setOnEmpty' => true,
                'on' => 'insert'
            ),
            array(
                'time_update',
                'default',
                'value' => new CDbExpression('NOW()'),
                'setOnEmpty' => false,
                'on' => 'update'
            ),
            array(
                'time_create, time_update',
                'default',
                'value' => new CDbExpression('NOW()'),
                'setOnEmpty' => false,
                'on' => 'insert'
            ),
            array(
                'time_renewal',
                'default',
                'value' => time(),
                'setOnEmpty' => false,
                'on' => 'update'
            ),
            array(
                'time_renewal',
                'default',
                'value' => time(),
                'setOnEmpty' => false,
                'on' => 'insert'
            ),
            array('time_begin, time_end', 'safe'),
            array(
                'id, pid, aid,app_id, bid, time,time_renewal, alive, retry, bundle, status, type, channel, params, result, time_create, time_update',
                'safe',
                'on' => 'search'
            ),
        );
    }

    /**
     * @return array relational rules.
     */
    public function relations()
    {
        return array(
            'msgChannelLogs' => array(self::HAS_MANY, 'MsggChannelLog', ['iid' => 'id'], 'index' => 'id'),
            'msgItemHooks' => array(self::HAS_MANY, 'MsggItemHook', ['iid' => 'id'], 'index' => 'id'),
            'msggApp' => array(self::HAS_ONE, 'MsggApp', ['id' => 'app_id']),
            'alarmPerson' => array(self::HAS_ONE, 'AlarmPerson', ['id' => 'pid']),
        );
    }

    public function beforeSave()
    {
        $this->params = json_encode($this->getParams());
        $this->result = json_encode($this->getResult());
        return parent::beforeSave();
    }

    protected function afterSave()
    {
        //insert
        if (is_array($this->_logs) && $this->id && $this->_logs) {
            foreach ($this->_logs as $log) {
                if ($log instanceof MsggChannelLog) {
                    $log->iid = $this->id;
                    $log->save();
                }
            }
        }

        parent::afterSave(); // TODO: Change the autogenerated stub
    }


    /**
     * @return array customized attribute labels (name=>label)
     */
    public function attributeLabels()
    {
        return array(
            'id' => 'ID',
            'pid' => 'Pid',
            'aid' => 'Aid',
            'app_id' => 'APP ID',
            'bid' => 'Bid',
            'time' => 'Time',
            'time_renewal' => 'TimeRenewal',
            'level' => 'Level',
            'alive' => 'Alive',
            'retry' => 'Retry',
            'bundle' => 'Bundle',
            'status' => 'Status',
            'type' => 'Type',
            'channel' => 'Channel',
            'params' => 'Params',
            'call_back_params' => 'Call Back Params',
            'result' => 'Result',
            'time_create' => 'Time Create',
            'time_update' => 'Time Update',
        );
    }

    public function init()
    {
        //attache hander
        $handlers = MsggItemHandlerFactory::builds(self::getOptions('eventType'));
        foreach ($handlers as $name => $handlerArray) {
            foreach ($handlerArray as $handler) {
                $this->attachEventHandler($name, $handler);
            }

        }
        parent::init(); // TODO: Change the autogenerated stub
    }

    private function onAfterExecute(CEvent $event)
    {
        $event->sender = $this;
        $event->params = array_merge([
            'eventType' => MsggItem::EVENT_TYPE_ON_AFTER_EXECUTE,
            'mergeCallSize' => MsggGlobalConfig::getGloabalConfigValue(MsggGlobalConfig::CONFIG_MERGE_CALL_SIZE, null)
        ], $event->params);
        $this->raiseEvent('onAfterExecute', $event);
    }

    private function onBeforeExecute(CEvent $event)
    {
        $event->sender = $this;
        $event->params = array_merge([
            'eventType' => MsggItem::EVENT_TYPE_ON_BEFORE_EXECUTE,
            'mergeCallSize' => MsggGlobalConfig::getGloabalConfigValue(MsggGlobalConfig::CONFIG_MERGE_CALL_SIZE, null)
        ], $event->params);
        $this->raiseEvent(MsggItem::EVENT_TYPE_ON_BEFORE_EXECUTE, $event);
    }

    public function onAfterHook(CEvent $event)
    {
        $event->sender = $this;
        $event->params = array_merge([
            'eventType' => MsggItem::EVENT_TYPE_ON_AFTER_HOOK,
            'mergeCallSize' => MsggGlobalConfig::getGloabalConfigValue(MsggGlobalConfig::CONFIG_MERGE_CALL_SIZE, null)
        ], $event->params);

        $this->raiseEvent('onAfterHook', $event);
    }

    /**
     * Retrieves a list of models based on the current search/filter conditions.
     *
     * Typical usecase:
     * - Initialize the model fields with values from filter form.
     * - Execute this method to get CActiveDataProvider instance which will filter
     * models according to data in model fields.
     * - Pass data provider to CGridView, CListView or any similar widget.
     *
     * @return CActiveDataProvider the data provider that can return the models
     * based on the search/filter conditions.
     */
    public function search()
    {
        $criteria = new CDbCriteria;

        $criteria->compare('id', $this->id);
        $criteria->compare('pid', $this->pid);
        $criteria->compare('aid', $this->aid);
        $criteria->compare('app_id', $this->app_id);
        $criteria->compare('bid', $this->bid);
        $criteria->compare('time', $this->time);
        $criteria->compare('time_renewal', $this->time_renewal);
        $criteria->compare('level', $this->level);
        $criteria->compare('status', $this->status);
        $criteria->compare('alive', $this->alive);
        $criteria->compare('retry', $this->retry);
        $criteria->compare('bundle', $this->bundle);
        $criteria->compare('type', $this->type, true);
        $criteria->compare('channel', $this->channel, true);
        $criteria->compare('params', $this->params, true);
        $criteria->compare('result', $this->result, true);
        $criteria->compare('time_create', $this->time_create, true);
        $criteria->compare('time_update', $this->time_update, true);

        return new CActiveDataProvider($this, array(
            'criteria' => $criteria,
        ));
    }

    public function searchByKey()
    {
        $criteria = new CDbCriteria;
        if (!isset($_GET['MsggItem_sort'])) {
            $criteria->order = 't.id DESC';
        }
        if (!empty($this->bid)) {
            $criteria->addColumnCondition(array('bid' => $this->bid));
        }
        if (!empty($this->aid)) {
            $criteria->addColumnCondition(array('aid' => $this->aid));
        }
        if (!empty($this->app_id)) {
            $criteria->addColumnCondition(array('app_id' => $this->app_id));
        }

        if (!empty($ts_begin = strtotime($this->time_begin))) {
            $criteria->addCondition('`time` >= ' . $ts_begin);
        }
        if (!empty($ts_end = strtotime($this->time_end))) {
            $criteria->addCondition('`time` <= ' . $ts_end);
        }
        if (!empty($this->type)) {
            $criteria->addColumnCondition(array('type' => $this->type));
        }
        if (!empty($this->pid)) {
            $criteria->addColumnCondition(['pid' => $this->pid]);
        }
        $mapStatus = self::getOptions('status');
        if (array_key_exists($this->status, $mapStatus)) {
            $criteria->addColumnCondition(array('status' => $this->status));
        }
        if ($this->searchKey !== '') {
            $criteria->addSearchCondition('channel', $this->searchKey, true, 'OR');
            $criteria->addSearchCondition('params', $this->searchKey, true, 'OR');
            $criteria->addSearchCondition('result', $this->searchKey, true, 'OR');
        }

        return new CActiveDataProvider($this, array(
            'criteria' => $criteria,
            'pagination' => array(
                'pageSize' => 5,
            ),
        ));
    }

    public function getParams()
    {
        if ($this->_params === null) {
            if (!($this->_params = json_decode($this->params, true))) {
                switch ($this->bundle) {
                    case self::BUNDLE_ITEM:
                        $this->_params = $this->getBundleParams();
                        break;
                    default:
                        $this->_params = array();
                        break;
                }
            }
        }
        return $this->_params;
    }

    public function getExtId()
    {
        $result = $this->getResult();
        $extid = null;
        if ($result) {
            $result = @end(self::util_array_get(end($result), 'detail', []));
            if ($result != false && is_array($result)) {
                $ext = self::util_array_get($result, 'ext', []);
                $extid = self::util_array_get($ext, 'id', null);
            }
        }
        return $extid;
    }

    public function set_Params(array $params)
    {
        $this->_params = $params;
    }

    public function getBundleParams()
    {
        $params = array();
        switch ($this->type) {
            case self::TYPE_MAIL:
                $subMsgs = self::model()->findAllByAttributes(array('bid' => $this->id));
                if (!empty($subMsgs)) {
                    $params['address'] = null;
                    $params['title'] = '[聚合邮件]共有' . count($subMsgs) . '封告警邮件';
                    $subLines = array();
                    foreach ($subMsgs as $subMsg) {
                        $subParams = $subMsg->getParams();
                        $subUrl = Yii::app()->createAbsoluteUrl('/msgg/item/view', array('id' => $subMsg->id));
                        $subLines[] = CHtml::link('#Msg-' . $subMsg->id,
                                $subUrl) . ' : ' . (empty($subParams['title']) ? 'Unknown Title' : $subParams['title']);
                        $params['address'] = empty($subParams['address']) ? $params['address'] : $subParams['address'];
                    }
                    $params['content'] = implode('<br>', $subLines);
                }
                break;
        }
        return $params;
    }
    public function set_Result($result)
    {
        if (is_array($result)) {
            $this->_result = $result;
        } else {
            $this->_result = json_decode($result, true);
        }
    }

    public function getResult()
    {
        if ($this->_result === null) {
            if (!($this->_result = json_decode($this->result, true)) || !is_array($this->_result)) {
                $this->_result = array();
            }
        }
        return $this->_result;
    }

    public function execute($sync = false, $channelConfigs = [])
    {
        $result = array('code' => 0, 'msg' => '');
        $itemResult = $this->getResult();
        $now = time();
        //make sure not to run again
        if (($this->isInitial()) && ($this->time <= $now)) {
            $this->status = self::STATUS_WAITING;
        }
        if ($this->isInitial()) {
            $result['code'] = self::ERROR_CODE_ITEM_STATUS_INIT_INVALID;
            $result['msg'] = self::ERROR_MSG_ITEM_STATUS_INIT_INVALID;
            return $result;
        }
        if ($this->isRunning() || $this->isHooking() || $this->isOver() || $this->isCancel()) {
            $result['code'] = 0;
            $result['msg'] = 'msg is already running or over or cancel';
            return $result;
        }
        //to running status
        $this->status = self::STATUS_RUNNING;
        if (!$sync) {
            $this->save();
        }
        $this->onBeforeExecute(new CEvent($this, []));
        //to execute
        try {
            if (empty($channelConfigs)) {
                $channelConfigs = ChannelConfig::getChannelConfigs($this->type, ['level' => $this->level]);
            }
            if (!is_array($channelConfigs)) {
                $this->status = self::mapCodeStatus(self::ERROR_CODE_ITEM_CONFIG_INVALID);
                throw new Exception(self::ERROR_MSG_ITEM_CONFIG_INVALID, self::ERROR_CODE_ITEM_CONFIG_INVALID);
            }
            if (!($params = $this->getParams())) {
                $this->status = self::mapCodeStatus(self::ERROR_CODE_ITEM_PARAMS_INVALID);
                throw new Exception(self::ERROR_MSG_ITEM_PARAMS_INVALID . ':decode fail', self::ERROR_CODE_ITEM_PARAMS_INVALID);
            }
            if ($this->checkPersonOverClock($now, true)) {
                $this->status = self::mapCodeStatus(self::ERROR_CODE_PERSON_OVER_CLOCK_ERROR);
                throw new Exception(self::ERROR_MSG_PERSON_OVER_CLOCK_ERROR, self::ERROR_CODE_PERSON_OVER_CLOCK_ERROR);
            }
            if ($this->checkTimeOut($now)) {
                $this->status = self::mapCodeStatus(self::ERROR_CODE_ITEM_TIME_OUT_ERROR);
                throw  new Exception(self::ERROR_MSG_ITEM_TIME_OUT_ERROR, self::ERROR_CODE_PERSON_OVER_CLOCK_ERROR);
            }
            $channelConfigs = $this->sortChannelConfigs($channelConfigs);
            switch ($this->type) {
                case self::TYPE_MAIL:
                    $result = MsggMail::execute($params, $channelConfigs);
                    break;
                case self::TYPE_SMS:
                    $result = MsggSms::execute($params, $channelConfigs);
                    break;
                case self::TYPE_WECHAT:
                    $result = MsggWechat::execute($params, $channelConfigs);
                    break;
                case self::TYPE_CALL:
                    $result = MsggCall::execute($params, $channelConfigs);
                    break;
                default:
                    $this->status = self::mapCodeStatus(self::ERROR_CODE_ITEM_TYPE_INVALID);
                    throw new Exception(self::ERROR_MSG_ITEM_TYPE_INVALID, self::ERROR_CODE_ITEM_TYPE_INVALID);
                    break;
            }
            $code = isset($result['code']) ? $result['code'] : null;
            $this->channel = empty($result['detail']) ? null : @end(array_keys($result['detail']));
            $channelClass = isset($result['ext']['class']) ? $result['ext']['class'] : null;
            switch ($this->type) {
                case self::TYPE_MAIL:
                    $this->status = MsggMail::mapCodeStatus($channelClass, $code);
                    break;
                case self::TYPE_WECHAT:
                    $this->status = MsggWechat::mapCodeStatus($channelClass, $code);
                    break;
                case self::TYPE_SMS:
                    $this->status = MsggSms::mapCodeStatus($channelClass, $code);
                    break;
                case self::TYPE_CALL:
                    $this->status = MsggCall::mapCodeStatus($channelClass, $code);
                    break;
                default:
                    $this->status = self::mapCodeStatus($code);
                    break;
            }
            if (!$this->_logs && isset($result['logs']) && is_array($result['logs'])) {
                $this->_logs = $result['logs'];
            }
            unset($result['logs']);
            unset($result['ext']);
        } catch (Exception $e) {
            Yii::log($e->getMessage(), CLogger::LEVEL_WARNING, __METHOD__);
            $result['code'] = $e->getCode();
            $result['msg'] = $e->getMessage();
        }
        $itemResult[] = $result;
        if (empty($params['debug']) || ($params['debug'] !== 'bsc')) {
            unset($result['detail']);
        }
        $this->_result = $itemResult;
        //if failure,check is alive and can be retry
        if (($this->isFailure() || $this->isPersonOverClock()) && (empty($this->time) || empty($this->alive) || ($now < (intval($this->time) + intval($this->alive)))) && (count($itemResult) <= $this->retry)) {
            $this->status = self::STATUS_RETRYING;
        }
        if (!$sync) {
            $this->save();
        }
        $this->onAfterExecute(new CEvent($this, []));
        return $result;
    }

    public function transmit($sync = false, $config = [])
    {
        return $this->execute($sync, $config);
    }

    public function sortChannelConfigs($channelConfigs = array())
    {
        if (empty($channelConfigs)) {
            return [];
        }
        $extid = $this->getExtId();
        if (!empty($extid)) {
            $channel = MsggChannelLog::model()->findByAttributes(['ext_id' => $extid])->channel;
            foreach ($channelConfigs as $i => $channelConf) {
                $channelName = implode('_', array($this->type, self::util_array_get($channelConf, 'name', 'Channel')));
                if ($channel == $channelName) {
                    if ($i == 0) {
                        $tmp = $channelConfigs[$i];
                        $size = count($channelConfigs);
                        if ($i < $size - 1) {
                            $channelConfigs[0] = $channelConfigs[$size - 1];
                            $channelConfigs[$size - 1] = $tmp;
                        }
                    }
                    break;
                }
            }
        }
        return $channelConfigs;
    }

    public function checkPersonOverClock($time = null, $add = false)
    {
        if (empty($time)) {
            $time = time();
        }
        $pre = '';
        $type = $this->type;
        $limit = self::getLimit($type, '?');
        $params = $this->getParams();
        switch ($this->type) {
            case self::TYPE_SMS:
                $pre = isset($params['phone']) ? $params['phone'] : '';
                break;
            case self::TYPE_WECHAT:
                $pre = isset($params['to']) && !is_array($params['to']) ? $params['to'] : '';
                break;
            case self::TYPE_CALL:
                $pre = isset($params['phone']) ? $params['phone'] : '';
                break;
            case self::TYPE_MAIL:
                $pre = isset($params['address']) && !is_array($params['address']) ? $params['address'] : '';
                break;
            default :
                $pre = $this->pid;
        }
        if (!empty($pre) && !empty($limit) && MsggItemValve::isOverClock($pre, $this->type, $limit, $time)) {
            return true;
        } else {
            if ($add && !empty($pre)) {
                MsggItemValve::add($pre, $this->type, $time);
            }
            return false;
        }


    }

    public function checkTimeOut($time = null)
    {
        if (empty($time)) {
            $time = time();
        }
        if (!$this->isOver() && !empty($this->time) && !empty($this->alive) && $this->alive != 0 && ($this->time + $this->alive) < $time) {
            return true;
        }
        return false;
    }

    public static function getLimit($type, $name = null, $durations = null)
    {
        $result = [];
        if (empty($name) || $name == '?') {
            $name = '?';
        }
        if ($durations == null) {
            $durations = ['minute', 'hour', 'day',];
        }
        foreach ($durations as $duration) {
            $key = implode('_', ['limit', $name, $type, $duration]);
            $defaultKey = implode('_', ['limit', '?', $type, $duration]);
            $result[$duration] = MsggGlobalConfig::getGloabalConfigValue($key, MsggGlobalConfig::getGloabalConfigValue($defaultKey, null));
        }
        return $result;
    }

    public function isInitial()
    {
        return in_array($this->status, array(self::STATUS_INITIAL));
    }

    public function isWaiting()
    {
        return in_array($this->status, array(self::STATUS_WAITING, self::STATUS_RETRYING));
    }

    public function isPersonOverClock()
    {
        return ($this->status == self::STATUS_PERSON_OVERCLOCK);
    }

    public function isRunning()
    {
        return ($this->status == self::STATUS_RUNNING);
    }

    public function isHooking()
    {
        return ($this->status == self::STATUS_HOOKING);
    }

    public function isComplete()
    {
        return $this->status == self::STATUS_COMPLETE;
    }

    public function isOver()
    {
        return in_array($this->status,
            array(self::STATUS_SUCCESS, self::STATUS_BUNDLED, self::STATUS_FAILURE, self::STATUS_ERROR, self::STATUS_COMPLETE));
    }

    public function isSuccess()
    {
        return ($this->status == self::STATUS_SUCCESS);
    }

    public function isBundled()
    {
        return ($this->status == self::STATUS_BUNDLED);
    }

    public function isTimeout()
    {
        return ($this->status == self::STATUS_TIMEOUT);
    }

    public function isFailure()
    {
        return ($this->status == self::STATUS_FAILURE);
    }

    public function isError()
    {
        return ($this->status == self::STATUS_ERROR);
    }

    public function isCancel()
    {
        return ($this->status == self::STATUS_CANCEL);
    }

    public function isRetrying()
    {
        return ($this->status == self::STATUS_RETRYING);
    }


    /**
     * Returns the static model of the specified AR class.
     * Please note that you should have this exact method in all your CActiveRecord descendants!
     * @param string $className active record class name.
     * @return MsggItem the static model class
     */
    public static function model($className = __CLASS__)
    {
        return parent::model($className);
    }

    public static function mapCodeStatus($code)
    {
        $status = self::STATUS_ERROR;
        switch ($code) {
            case self::ERROR_CODE_GATEWAY_FAILURE:
                $status = self::STATUS_FAILURE;
                break;
            case self::ERROR_CODE_ITEM_TIME_OUT_ERROR:
                $status = self::STATUS_TIMEOUT;
                break;
            case self::ERROR_CODE_PERSON_OVER_CLOCK_ERROR:
                $status = self::STATUS_PERSON_OVERCLOCK;
                break;
            case self::ERROR_CODE_ITEM_ATTR_INVALID:
            case self::ERROR_CODE_ITEM_TYPE_INVALID:
            case self::ERROR_CODE_ITEM_PARAMS_INVALID:
            case self::ERROR_CODE_ITEM_CONFIG_INVALID:
            default:
                $status = self::STATUS_ERROR;
                break;
        }
        return $status;
    }

    public static function getOptions($attribute, $options = array())
    {
        if (!isset(self::$_options[$attribute])) {
            $opts = [];
            switch ($attribute) {
                case 'type':
                    $opts[self::TYPE_MAIL] = '邮件';
                    $opts[self::TYPE_CALL] = '电话';
                    $opts[self::TYPE_SMS] = '短信';
                    $opts[self::TYPE_WECHAT] = '微信';
                    break;
                case 'status':
                    $opts[self::STATUS_SUCCESS] = '发送成功';
                    $opts[self::STATUS_BUNDLED] = '聚合发送';
                    $opts[self::STATUS_CANCEL] = '取消发送';
                    $opts[self::STATUS_FAILURE] = '发送失败';
                    $opts[self::STATUS_ERROR] = '发送错误';
                    $opts[self::STATUS_RUNNING] = '发送中';
                    $opts[self::STATUS_COMPLETE] = '发送完成';
                    $opts[self::STATUS_PERSON_OVERCLOCK] = '个人发送超频';
                    $opts[self::STATUS_TIMEOUT] = '发送超时';
                    $opts[self::STATUS_HOOKING] = '回调中';
                    $opts[self::STATUS_WAITING] = '等待中';
                    $opts[self::STATUS_RETRYING] = '重试中';
                    $opts[self::STATUS_INITIAL] = '初始化';
                    break;
                case 'pid':
                    break;
                case 'try_time':
                    $opts[self::TYPE_MAIL] = 2;
                    $opts[self::TYPE_SMS] = 3;
                    $opts[self::TYPE_WECHAT] = 3;
                    $opts[self::TYPE_CALL] = 5;
                    break;
                case 'level':
                    break;
                case 'eventType':
                    $opts[self::EVENT_TYPE_ON_BEFORE_EXECUTE] = self::EVENT_TYPE_ON_BEFORE_EXECUTE;
                    $opts[self::EVENT_TYPE_ON_AFTER_EXECUTE] = self::EVENT_TYPE_ON_AFTER_EXECUTE;
                    $opts[self::EVENT_TYPE_ON_AFTER_HOOK] = self::EVENT_TYPE_ON_AFTER_HOOK;
                    break;

            }
            self::$_options[$attribute] = $opts;
        }
        if (is_array($options) && !empty($options)) {
            foreach (self::$_options[$attribute] as $k => $v) {
                $options[$k] = $v;
            }
        } else {
            $options = self::$_options[$attribute];
        }
        return $options;
    }

    public static function build($type, $params, $attrs = array())
    {
        $item = new self();
        $item->pid = empty($attrs['pid']) ? null : $attrs['pid'];
        $item->aid = empty($attrs['aid']) ? 0 : $attrs['aid'];
        $item->app_id = empty($attrs['app_id']) ? null : $attrs['app_id'];
        $item->time = empty($attrs['time']) ? time() : $attrs['time'];
        $item->level = empty($attrs['level']) ? 0 : $attrs['level'];
        $item->alive = empty($attrs['alive']) ? 0 : $attrs['alive'];
        $item->retry = empty($attrs['retry']) ? 0 : $attrs['retry'];
        $item->call_back_params = json_encode(empty($attrs['call_back_params']) ? [] : $attrs['call_back_params'], true);
        $item->bundle = empty($attrs['bundle']) ? self::BUNDLE_DENY : $attrs['bundle'];
        $item->status = self::STATUS_INITIAL;
        $item->type = $type;
        $item->_params = $params;
        $person = null;
        switch ($type) {
            case self::TYPE_MAIL:
                if (isset($params['address']) && ($mail = $params['address']) && !is_array($mail)) {
                    $person = AlarmPerson::model()->find('`mail`=:mail', [":mail" => $mail]);
                }
                break;
            case self::TYPE_WECHAT:
                if (isset($params['to']) && ($wechat = $params['to']) && !is_array($wechat)) {
                    $person = AlarmPerson::model()->find('`wechat`=:wechat', [":wechat" => $wechat]);
                }
                break;
            case self::TYPE_SMS:
            case self::TYPE_CALL:
                if (isset($params['phone']) && ($phone = $params['phone']) && !is_array($phone)) {
                    $person = AlarmPerson::model()->find('`phone`=:phone', [":phone" => $phone]);
                }
                break;
            default :
                break;
        }
        $item->pid = empty($person) ? $item->pid : $person->id;
        return $item->validate() ? $item : null;
    }

    //for sync api
    public static function send($params, $type = null, $attrs = array(), $sync = true)
    {
        $result = array('code' => 0, 'msg' => '');
        try {
            $items = array();
            switch ($type) {
                case self::TYPE_MAIL:
                case self::TYPE_CALL:
                case self::TYPE_SMS:
                case self::TYPE_WECHAT:
                    $items[$type] = self::build($type, $params, $attrs);
                    break;
                default:
                    $types = array_keys(self::getOptions('type'));
                    foreach ($types as $type) {
                        if (isset($params[$type])) {
                            $items[$type] = self::build($type, $params[$type], $attrs);
                        }
                    }
                    break;
            }
            $numError = 0;
            $numFailure = 0;
            $numAll = count($items);
            $result['data'] = array();
            foreach ($items as $type => $item) {
                if (!$item) {
                    $numError++;
                    $result['data'][$type] = array(
                        'code' => self::ERROR_CODE_ITEM_ATTR_INVALID,
                        'msg' => self::ERROR_MSG_ITEM_ATTR_INVALID,
                    );
                }
                $try = 0;
                $msgResult = array();
                if ($sync) {
                    while (($item->isInitial() || $item->isWaiting()) && ($try <= $item->retry)) {
                        $try++;
                        $transmitter = new AppItemTransmitterDecorator($item);
                        $msgResult = $transmitter->execute($sync);
                    }
                    $item->save();
                    $msgResult['id'] = $item->id;
                    $result['data'][$type] = $msgResult;
                    if (!isset($msgResult['code']) || ($msgResult['code'] !== 0)) {
                        $numFailure++;
                    }
                } else {
                    $item->save();
                    $msgResult['code'] = 0;
                    $msgResult['id'] = $item->id;
                    $result['data'][$type] = $msgResult;
                }

            }
            if ($numError || !$numAll) {
                $result['code'] = self::ERROR_CODE_SEND_ERROR;
                $result['msg'] = (self::ERROR_MSG_SEND_ERROR) . $numAll ? (($numAll == $numError) ? 'all msgs error' : 'some msgs error') : 'no msgs';
            } elseif ($numFailure) {
                $result['code'] = self::ERROR_CODE_SEND_FAILURE;
                $result['msg'] = (self::ERROR_MSG_SEND_FAILURE) . ($numFailure == $numAll) ? 'all msgs fail' : 'some msgs fail';
            }
        } catch (Exception $e) {
            $result['code'] = $e->getCode();
            $result['msg'] = $e->getMessage();
        }
        return $result;
    }

    //for inner api
    public static function activeMsgs($ts = null)
    {
        if (empty($ts)) {
            $ts = time();
        }
        $criteria = new CDbCriteria();
        $criteria->addColumnCondition(array('`status`' => self::STATUS_INITIAL));
        $criteria->addCondition('`time` <= ' . $ts);
        //default 86400
        $duration = MsggGlobalConfig::getGloabalConfigValue(MsggGlobalConfig::CONFIG_ACTIVEMSG_DURATION, 86400);
        $end = $ts - $duration;
        $criteria->addCondition('`time` <= ' . $ts);
        $criteria->addCondition('`time` >= ' . $end);
        self::model()->updateAll(array('status' => self::STATUS_WAITING), $criteria);
    }

    //for inner api
    public static function getMsgs($type, $limit = 100)
    {
        try {
            $strategy = MsggGlobalConfig::getGloabalConfigValue(MsggGlobalConfig::CONFIG_GET_MSG_STRATEGY, 'default');
            $items = array();
            $strategyInstance = GetMsgStrategyFactory::builds($type, $limit, $strategy);
            if ($strategyInstance instanceof GetMsgStrategy) {
                $items = $strategyInstance->getItemList();
            } else {
                Yii::log('GetMsgStrategyFactory can\'t build strategy ' . $type . $strategy, CLogger::LEVEL_ERROR, __METHOD__);
            }
        } catch (Exception $e) {
            Yii::log($e->getMessage(), CLogger::LEVEL_ERROR, __CLASS__ . __METHOD__);
        }
        return $items;
    }

    //for api
    public static function refreshStatus($time = null, $duration = 86400)
    {
        if (empty($time)) {
            //default
            $time = time() - 2 * 60;
        }
        $criteria = new CDbCriteria();
        $criteria->addInCondition('`status`', [MsggItem::STATUS_HOOKING, MsggItem::STATUS_RUNNING]);
        $criteria->addCondition('`time_renewal` <= ' . $time);
        $criteria->addCondition('`time_renewal` >= ' . $time - $duration);
        $criteria->limit = '1000';
        $criteria->order = 'time';
        $msgs = MsggItem::model()->findAll($criteria);
        foreach ($msgs as $msg) {
            switch ($msg->type) {
                case MsggItem::TYPE_CALL:
                    $status = MsggCall::refreshStatus($msg);
                    break;
                case MsggItem::TYPE_SMS:
                    $status = MsggSms::refreshStatus($msg);
                    break;
                case MsggItem::TYPE_MAIL:
                    $status = MsggMail::refreshStatus($msg);
                    break;
                case MsggItem::TYPE_WECHAT:
                    $status = MsggWechat::refreshStatus($msg);
                    break;
                default:
                    $status = MsggItem::STATUS_TIMEOUT;
            }
            $msg->status = $status;
            //status in hooking or running
            if ($msg->isHooking() || $msg->isRunning()) {
                $msg->status = MsggItem::STATUS_TIMEOUT;
            }
            if ($msg->isFailure() || $msg->isTimeout()) {
                if ((count($msg->getResult()) <= $msg->retry) || ($msg->retry == 0 && count($msg->getResult()) <= 1)) {
                    $msg->status = MsggItem::STATUS_RETRYING;
                }
            }
            $msg->save();
        }
    }

    //for api
    public static function mergeUnreachableMsgs($params)
    {
        $status = [MsggItem::STATUS_TIMEOUT, MsggItem::STATUS_PERSON_OVERCLOCK, MsggItem::STATUS_ERROR, MsggItem::STATUS_FAILURE];
        $types = array_keys(MsggItem::getOptions('type'));
        $date = MsggGlobalConfig::getGloabalConfigValue(MsggGlobalConfig::CONFIG_MERGE_MSG_DATE_CREATE, null);
        $start = empty($date) ? time() : strtotime($date);
        try {
            $bundler = new MsggBundler($types, $status, 0, null, $start, time(),true,false);
            $bundler->setGenerator(
                function ($pid, $ids, $bundler) {
                    return "msgg 提醒您有信息未能成功发送请及时查看！！";
                },
                function ($pid, $ids, $bundler) {
                    $params = array();
                    if ($bundler instanceof MsggBundler) {
                        //find really types
                        $criteria = new CDbCriteria();
                        $criteria->select = 'type';
                        $criteria->addInCondition('id',$ids);
                        $criteria->group = 'type';
                        $types = array_map(function ($item) {
                            return $item->type;
                        }, MsggItem::model()->findAll($criteria));
                        $params[] = ['label' => '描述', 'value' => 'msgg 提示您有部分信息未能成功发送！！'];
                        $params[] = ['label' => '截止时间', 'value' => date('Y-h-d H:m:s', $bundler->getBundleTimeEnd())];
                        $params[] = ['label' => '失败信息类型', 'value' => is_array($types) ? implode(',', $types) : ''];
                        $params[] = ['label' => '失败信息数量', 'value' => sizeof($ids),
                        ];
                    }
                    return $params;
                });
            $bundler->bundle();
        } catch (Exception $e) {
            Yii::log($e->getMessage(), $e->getCode(), __CLASS__ . __METHOD__);
            return false;
        }
        return true;
    }



    public function updateStatus($status, $save = true, $checkRetry = true)
    {
        $mapStatus = self::getOptions('status');
        if (array_key_exists($status, $mapStatus)) {
            $this->status = $status;
        }
        if ($checkRetry) {
            if (($this->isFailure() || $this->isPersonOverClock())
                && (empty($this->time) || empty($this->alive) || (time() < (intval($this->time) + intval($this->alive))))
                && (count($this->getResult()) <= $this->retry)) {
                $this->status = self::STATUS_RETRYING;
            }
        }
        if ($save) {
            $this->save();
        }
    }
}
